/**
 * Anderground project - Subway maps processor application.
 * Copyright (C) 2008-2009 Eldar Abusalimov, Ilya Shurigyn
 * 
 * This file is part of Anderground.
 * 
 * Anderground is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * Anderground is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Anderground.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.anderground.core.util;

/**
 * @see Binder
 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
 */
public abstract class Bindable {

	private Binder binder;

	/**
	 * Binds this {@link Bindable} to the specified {@link Binder} invoking
	 * {@link Binder#bind(Bindable)} on it with this Bindable as the argument.
	 * <p>
	 * The {@link #onBindTo(Binder)} callback method will be invoked just after
	 * this will be actually bound to the Binder.
	 * 
	 * @param binder
	 *            the binder to bind this bindable to.
	 * @throws BindingException
	 *             if the receiver has already been bound with another Binder
	 *             than passed in the argument
	 * @throws IllegalArgumentException
	 *             if the passed binder is null
	 */
	public final void bindTo(Binder binder) throws BindingException {
		if (binder == null) {
			throw new IllegalArgumentException("binder cannot be null");
		}
		if (this.binder == binder) {
			// the object has already been bound with this binder
			// noop, silently return
			return;
		}
		if (this.binder != null) {
			throw new BindingException("object " + this
					+ " has already been bound with another binder");
		}

		try {
			this.binder = binder;
			this.onBindTo(binder);
		} catch (BindingException e) {
			// rollback on failure
			this.binder = null;
			throw e;
		}

		binder.bind(this);
	}

	/**
	 * Unbinds this {@link Bindable} from the specified {@link Binder} invoking
	 * {@link Binder#unbind(Bindable)} on it with this Bindable as the argument.
	 * <p>
	 * The {@link #onUnbindFrom(Binder)} callback method will be invoked just
	 * before this will be actually unbound from the Binder.
	 * 
	 * @param binder
	 *            the binder to unbind this bindable from.
	 * @throws BindingException
	 *             if the receiver has been bound with another Binder than
	 *             passed in the argument
	 * @throws IllegalArgumentException
	 *             if the passed binder is null
	 */
	public final void unbindFrom(Binder binder) throws BindingException {
		if (binder == null) {
			throw new IllegalArgumentException("binder cannot be null");
		}
		if (this.binder == null) {
			// the object has already been unbound
			// noop, silently return
			return;
		}
		if (this.binder != binder) {
			throw new BindingException("object " + this
					+ " has been bound with another binder");
		}

		this.onUnbindFrom(binder);

		binder.unbind(this);
		this.binder = null;
	}

	/**
	 * Requests the receiver to bind with another {@link Bindable}.
	 * 
	 * @param bindable
	 *            the Bindable to bind with
	 * @throws BindingException
	 *             if the receiver has not yet been bound to any {@link Binder}
	 *             or if it has been bound with another Binder than the argument
	 * @throws IllegalArgumentException
	 *             if the passed bindable is null
	 */
	public final void bindWith(Bindable bindable) throws BindingException {
		if (bindable == null) {
			throw new IllegalArgumentException("bindable cannot be null");
		}
		if (binder == null) {
			throw new BindingException("object " + this
					+ " has not yet been bound");
		}
		if (binder != bindable.binder) {
			throw new BindingException("object " + this
					+ " has not been bound with the same binder as " + bindable);
		}

		this.onBindWith(bindable);
	}

	/**
	 * Requests the receiver to remove binding with another {@link Bindable}.
	 * 
	 * @param bindable
	 *            the Bindable to remove binding with
	 * @throws BindingException
	 *             if the receiver has not yet been bound to any {@link Binder}
	 *             or if it has been bound with another Binder than the argument
	 * @throws IllegalArgumentException
	 *             if the passed bindable is null
	 */
	public final void unbindWith(Bindable bindable) throws BindingException {
		if (bindable == null) {
			throw new IllegalArgumentException("bindable cannot be null");
		}
		if (binder == null) {
			throw new BindingException("object " + this
					+ " has not yet been bound");
		}
		if (binder != bindable.binder) {
			throw new BindingException("object " + this
					+ " has not been bound with the same binder as " + bindable);
		}

		this.onUnbindWith(bindable);
	}

	/**
	 * @return the binder
	 */
	protected final Binder getBinder() {
		return binder;
	}

	/**
	 * @return true if this {@link Bindable} has been bounded to some
	 *         {@link Binder}, or false otherwise
	 */
	public final boolean isBound() {
		return binder != null;
	}

	/**
	 * Called from {@link #bindTo(Binder)} just after the object was actually
	 * bound to the {@link Binder}.
	 * 
	 * @param binder
	 * @throws BindingException
	 */
	protected void onBindTo(Binder binder) throws BindingException {
	}

	/**
	 * Called from {@link #unbindFrom(Binder)} just before the object will be
	 * actually unbound from the {@link Binder}.
	 * 
	 * @param binder
	 * @throws BindingException
	 */
	protected void onUnbindFrom(Binder binder) throws BindingException {
	}

	/**
	 * Called from {@link #bindWith(Bindable)} invoked by a request of another
	 * {@link Bindable}.
	 * 
	 * @param bindable
	 * @throws BindingException
	 */
	protected void onBindWith(Bindable bindable) throws BindingException {
	}

	/**
	 * Called from {@link #unbindWith(Bindable)} invoked by a request of another
	 * {@link Bindable}.
	 * 
	 * @param bindable
	 * @throws BindingException
	 */
	protected void onUnbindWith(Bindable bindable) throws BindingException {
	}

}
